﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NeuralNetworkPro.Activation_Functions;

namespace NeuralNetworkPro
{
    [Serializable]
    public class NeuralNetwork
    {
        public Layer[] HiddenLayers
        {
            get
            {
                return this.layers.Skip(1).Take(this.layers.Length - 2).ToArray();
            }
        }

        /// <summary>
        /// Network's inputs count
        /// </summary>
        protected int inputsCount;

        /// <summary>
        /// Network's layers count
        /// </summary>
        protected int layersCount;

        /// <summary>
        /// Network's layers
        /// </summary>
        protected Layer[] layers;

        /// <summary>
        /// Network's output vector
        /// </summary>
        protected double[] output;

        /// <summary>
        /// Network's inputs count
        /// </summary>
        public int InputsCount
        {
            get { return inputsCount; }
        }

        /// <summary>
        /// Network's layers count
        /// </summary>
        public int LayersCount
        {
            get { return layersCount; }
        }

        private IActivationFunction activationFunction;

        public IActivationFunction ActivationFunction
        {
            get { return activationFunction; }
            private set { activationFunction = value; }
        }
        

        /// <summary>
        /// Network's output vector
        /// </summary>
        /// 
        /// <remarks>The calculation way of network's output vector is determined by
        /// inherited class.</remarks>
        /// 
        public double[] Output
        {
            get { return output; }
        }

        /// <summary>
        /// Network's layers accessor
        /// </summary>
        /// 
        /// <param name="index">Layer index</param>
        /// 
        /// <remarks>Allows to access network's layer.</remarks>
        /// 
        public Layer this[int index]
        {
            get { return layers[index]; }
        }

        /// <summary>
        /// Compute output vector of the network
        /// </summary>
        /// 
        /// <param name="input">Input vector</param>
        /// 
        /// <returns>Returns network's output vector</returns>
        /// 
        /// <remarks>The actual network's output vecor is determined by inherited class and it
        /// represents an output vector of the last layer of the network. The output vector is
        /// also stored in <see cref="Output"/> property.</remarks>
        /// 
        public virtual double[] Compute(double[] input)
        {
            output = /*(new double[] { 1 }).Concat*/(input).ToArray();

            // compute each layer
            for (int i = 0; i < layersCount; i++)
            {
                output = layers[i].Compute(output);
            }
            //foreach (Layer layer in layers)
            //{
            //    output = layer.Compute(output);
            //}

            return output;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Network"/> class
        /// </summary>
        /// <param name="function">Activation function of neurons of the network</param>
        /// <param name="inputsCount">Network's inputs count</param>
        /// <param name="neuronsCount">Array, which specifies the amount of neurons in
        /// each layer of the neural network</param>
        /// 
        /// <example>The following sample illustrates the usage of <c>Network</c> class:
        /// <code>
        ///		// create  network
        ///		Network network = new Network(
        ///			new SigmoidFunction( ), // sigmoid activation function
        ///			3,                      // 3 inputs
        ///			4, 1 );                 // 2 layers:
        ///                                 // 4 neurons in the firs layer
        ///                                 // 1 neuron in the second layer
        ///	</code>
        /// </example>
        /// 
        public NeuralNetwork(IActivationFunction function, int inputsCount, int[] neuronsCount)
        {
            var layer_count = (new int[] { inputsCount }).Concat(neuronsCount).ToArray();
            this.layersCount = layer_count.Length;
            this.inputsCount = Math.Max(1, inputsCount);
            this.activationFunction = function;
            // create collection of layers
            layers = new Layer[this.layersCount];
            layers[0] = Layer.InputLayer(layer_count[0], inputsCount, function);

            // create each layer
            for (int i = 1; i < this.layersCount; i++)
            {
                layers[i] = new Layer(
                    i,
                    // neurons count in the layer
                    layer_count[i],
                    // inputs count of the layer
                    layer_count[i - 1],
                    // activation function of the layer
                    function);
            }
        }

        internal void RemoveNeuron(int layerIdx, int neuronidx)
        {
            if (layerIdx >= this.layersCount - 1 || layerIdx < 1)
            {
                throw new Exception("Error in dimension");
            }

            this[layerIdx].RemoveNeuron(neuronidx);
            this[layerIdx+1].HasRemovedNeuron(neuronidx);

        }

        internal int HiddenNeurons()
        {
            int n = 0;
            var hiddens = this.HiddenLayers;
            foreach (var layer in hiddens)
            {
                n += layer.NeuronsCount;
            }
            return n;
        }

        public override string ToString()
        {
            string res = "";
            for (int i = 0; i < this.layersCount; i++)
			{
                res+= "Layer " + i + ": " + this.layers[i].NeuronsCount + "\n";
			}
            return res;
        }

        #region Save and load functions
        public void BinarySave(string name)
        {
            var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            var stream = new StreamWriter(name + ".sav");
            stream.BaseStream.Position = 0L;
            formatter.Serialize(stream.BaseStream, this);
            stream.Close();
            stream.Dispose();
        }

        public static NeuralNetwork BinaryLoad(string name)
        {
            var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            var stream = new System.IO.StreamReader(name + ".sav");
            stream.BaseStream.Position = 0L;
            var res = formatter.Deserialize(stream.BaseStream) as NeuralNetwork;
            stream.Close();
            stream.Dispose();
            return res;
        }

        private double[][][] ToDoubleArray()
        {
            double [][][] res= new double[layersCount][][];
            for (int i = 0; i < layersCount; i++)
            {
                var layer = layers[i];
                res[i] = new double[layer.NeuronsCount][];
                for (int j = 0; j < layer.NeuronsCount; j++)
                {
                    var neuron = layer[j];
                    res[i][j] = new double[neuron.InputsCount];
                    for (int k = 0; k < neuron.InputsCount; k++)
                    {
                        res[i][j][k] = neuron[k];
                    }
                }
            }
            return res;
        }

        private string DimensionToString()
        {
            string res = "";
            res += layersCount + ";" + inputsCount;
            for (int i = 1; i < layersCount; i++)
            {
                res += ";" + layers[i].NeuronsCount;
            }
            return res;
        }

        private static int[] StringToDimension(string data)
        {
            var elems = data.Split(new char[] { ';' });
            int[] res = new int[elems.Length];
            for (int i = 0; i < elems.Length; i++)
            {
                res[i] = int.Parse(elems[i]);
            }
            return res;
        }

        internal void ToStream(StreamWriter stream)
        {
            //List<string> res = new List<string>();
            var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            //stream.WriteLine(Utils.NetworkHeader);
            formatter.Serialize(stream.BaseStream, activationFunction.ToString());
            formatter.Serialize(stream.BaseStream, DimensionToString());
            formatter.Serialize(stream.BaseStream, (this.ToDoubleArray()));//.ToCSVStrings())
        }

        internal static NeuralNetwork FromStream(StreamReader stream)
        {
            var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            var activationFunction = Utils.DeserializeFunction(formatter.Deserialize(stream.BaseStream) as string);
            var dims = StringToDimension(formatter.Deserialize(stream.BaseStream) as string);
            var res = new NeuralNetwork(activationFunction, dims[1], dims.Skip(2).ToArray());
            
            //stream.DiscardBufferedData();
            var weights = formatter.Deserialize(stream.BaseStream) as double[][][];
            for (int i = 0; i < res.layersCount; i++)
            {
                var layer = res[i];
                for (int j = 0; j < layer.NeuronsCount; j++)
                {
                    var neuron = layer[j];
                    for (int k = 0; k < neuron.InputsCount; k++)
                    {
                        res[i][j][k] = weights[i][j][k];
                    }
                }
            }
            return res;
        }

        public void Save(string name)
        {
            var stream = new StreamWriter(name + ".sav");
            this.ToStream(stream);
            stream.Flush();
            stream.Close();
            //System.IO.File.WriteAllLines(name + ".sav", strs.ToArray());
        }
        public static NeuralNetwork Load(string name)
        {
            var stream = new StreamReader(name + ".sav");
            stream.BaseStream.Seek(0, SeekOrigin.Begin);
            return FromStream(stream);
        }

        public void Backup()
        {
            this.Save("backup");
        }

        public static NeuralNetwork Restore()
        {
            return Load("backup");
        }
        #endregion
    }
}
